%% @author Hee Yeon Cho <aposto@nexon.co.kr>
%% @copyright 2009 Nexon Corp.

%% @doc 
%%      
 
-module(savage_sessionman).
-behaviour(gen_server).

-author('aposto@nexon.co.kr').

-include_lib("stdlib/include/qlc.hrl").
-include("session.hrl").
-include("engine.hrl").

-define(NOTEST, true).
-include_lib("eunit/include/eunit.hrl").

%% External API
-export([start/0, start_link/0, stop/0, restart/0, dev_start/0]).

-export([new_connection/1, auth_session/2, reg_udp_session/2, message_to/2]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
		 terminate/2, code_change/3]).

-record(state, {users}).

-define(CONN_DB, connection_db).
-define(SESS_DB, session_db).

%% @spec test() -> ok
%% @doc Run all of the tests.
dev_start() ->
	stop(),
	timer:sleep(100),
	start_link(),
	new_connection(75),
	auth_session(75, "aposto"),
	destroy_session(100),
	
	timer:sleep(100),
	ClientPid = find_pid(75),
	exit(ClientPid, kill ),
	ok.
	
start() ->
	gen_server:start({local, ?MODULE}, ?MODULE, [], []).

start_link() ->
	gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).	

stop() ->
	gen_server:cast( ?MODULE, stop ). %%savage_sup:stop().

restart() ->
	stop(),
	start().	
 
init([]) ->
	process_flag(trap_exit, true),
	error_logger:info_msg("~p starting~n", [?MODULE]),
	ets:new( ?CONN_DB, [ protected, set, named_table, {keypos, #connection.sock} ] ),	
	ets:new( ?SESS_DB, [ protected, set, named_table, {keypos, #session.index} ] ),	
	Pid = spawn_link(fun() -> loop() end),
	{ok, #state{users=0}}.
	
handle_info({'DOWN', _, _, DownPid, _}, State) ->
	io:format("* handle_info DOWN ~p~n", [DownPid]),
	destroy_session( find_sock(DownPid) ),
	{noreply, State};
handle_info({'EXIT', Pid, _Reason}, State) ->
	{noreply, State};
handle_info(Msg, State) ->
	error_logger:error_msg("Unexpected message: ~p~n", [Msg]),
	exit({unknown_message, Msg}).
	%{noreply, State}.

terminate(Reason, State) ->	
	io:format("#terminate ~p:~p~n", [ Reason, State ]),
	Size0 = ets:info(?CONN_DB, size),		
	Size1 = ets:info(?SESS_DB, size),	
	ets:foldr( fun(X,_) -> close_session(X) end, [], ?CONN_DB), 
	io:format("# shutdown session manager ~p:~p:~p~n", [Size0, Size1, self()]),
	ets:delete(?CONN_DB),
	ets:delete(?SESS_DB),
	ok.

code_change(_OldVersion, State, _Extra) -> {ok, State}.

%%% 
new_connection(Sock) ->
	gen_server:cast( ?MODULE, {connect, Sock} ).

auth_session(Sock, Name) ->
	gen_server:cast( ?MODULE, {auth, Sock, Name} ).

reg_udp_session(Uid, Sock) ->
	gen_server:cast( ?MODULE, {reg_udp, Uid, Sock} ).

message_to(To, Data) ->
	gen_server:cast( ?MODULE, {message_to, To, Data} ).

 
%with tcp-socket
% pre must killed session process
destroy_session(Sock) ->
	case ets:lookup( ?CONN_DB, Sock ) of
		[#connection{index=Index}] ->
			io:format("* destroy connection ~p:~p~n", [Sock,Index]),
			ets:delete(?SESS_DB, Index),
			ets:delete(?CONN_DB, Sock);
		_ ->
			nop
	end.

loop() ->
	receive		
		{'EXIT', ClientPid, Reason} ->
			case Reason of
				{_, kill} ->
					io:format("# 'EXIT' client process with shutdown system ~p~n", [ClientPid] );
				{Sock, Error} ->
					io:format("# 'EXIT' client process with error ~p:~p~n", [ClientPid,Error] ),
					destroy_session(Sock);
				Any ->
					io:format("# 'EXIT' client process unknown exception ~p:~p~n", [ClientPid, Any] )
			end,
			loop();
		_ -> nil
	end.
	
%% Handle Message
handle_call(_Request, _From, State) ->
	{reply, ok, State}.

handle_cast( stop, State ) ->	
	{stop,normal,State};
	
handle_cast( {connect, Sock}, State ) ->	
	false = ets:member(?CONN_DB, Sock),
	ClientMod = savage_client:new(Sock),
	ClientPid = proc_lib:spawn_link( fun() -> ClientMod:init() end ),
	erlang:monitor(process, ClientPid),
	NewConnection = #connection{sock=Sock, pid=ClientPid, connect_time=now()},
	ets:insert( ?CONN_DB, NewConnection ),
	io:format("* new connection ~p:~p:~p~n", [Sock, ClientPid, ets:lookup(?CONN_DB,Sock)]),
	{noreply, State};
	
handle_cast({auth, Sock, Name}, State) ->
	true = ets:member(?CONN_DB, Sock),	
	case ets:lookup(?CONN_DB, Sock) of
		[] -> 
			noop;
		[C] ->
			undefined = C#connection.index,	
			Uid = savage_util:make_hash(Name),	
			NewSession = #session{index=Uid, tcp=Sock, pid=C#connection.pid, name=Name, last_recv_time=now()},		
			ets:insert_new(?SESS_DB, NewSession),	

			ets:update_element( ?CONN_DB, Sock, { #connection.index, Uid} ),
			%ets:delete( ?CONN_DB, Sock ), % remove connection
			io:format("* auth session ~p:~p~n", [Uid, ets:lookup(?SESS_DB,Uid)]),
			NewSession#session.pid ! {status, NewSession}
	end,
	{noreply, State};

handle_cast({reg_udp, Uid, Sock}, State) ->
	true = ets:member(?SESS_DB, Uid), %exception
	ets:update_element(?SESS_DB, Uid, { #session.udp, Sock}),		
	io:format("* reg_udp session ~p:~p~n", [Uid, ets:lookup(?SESS_DB, Uid)]),	
	{noreply, State};

handle_cast({message_to, To, Data}, _State) ->
	{[S],_} = ets:match_object(?SESS_DB, #session{name=To, _='_'}, 1),
	S#session.pid ! {recv, Data}.
%% Internal API

close_session(#connection{sock=Sock}) ->
	io:format("* delete session ~p~n", [Sock]),
	%gen_tcp:close(Sock),
	case find_uid(Sock) of
		0 -> noop;
		Uid -> ets:delete(?SESS_DB, Uid)
	end,	
	ets:delete(?CONN_DB,Sock), 
	ok.
	
find_uid(Sock) ->	
	case ets:lookup(?CONN_DB, Sock) of
		[] -> nil;
		[C] -> C#connection.index
	end.
	
find_pid(Sock) ->	
	case ets:lookup(?CONN_DB,Sock) of
		[] -> nil;
		[C] -> C#connection.pid
	end.
	
find_sock(Pid) when is_pid(Pid) ->
	{[C],_} = ets:match_object(?CONN_DB, #connection{pid=Pid, _='_'}, 1),
	C#connection.sock;
find_sock(_) ->
	nil.